1장. 랭체인이라는 레고 상자

출처: 『RAG 마스터: 랭체인으로 완성하는 LLM 서비스』(브라이스 유·조경아·박수진·김재웅 지음, 프리렉 2025) | 공식: www.langchain.com

코드는 분위기만 — pip install·|·invoke 같은 말은 몰라도 됩니다. 표의 '비유'와 '위험'만 봐도 충분해요.

이 장은 0장에서 배운 부품(LLM·검색기·임베딩)을 손으로 끼워 맞추게 해 주는 도구, 랭체인을 본다.

레고 상자를 처음 열듯, 어떤 블록이 들어 있고 그 블록을 어떻게 잇는지 천천히 살핀다.


0. 이 장의 새 단어 (0장에 없는 것만 3개)

0장 용어집에 있는 말(LLM·RAG·임베딩·체인·프롬프트·토큰·온도 등)은 다 안다고 보고 간다.

이 장에서 처음 나오는 말은 딱 3개다. 먼저 풀어 둔다.


러너블(runnable)

한 문장 뜻 — 랭체인에서 실행할 수 있는 작업 한 칸. 프롬프트도, 모델도, 파서도 전부 이 한 칸으로 똑같이 생겼다.

일상비유 — 같은 규격의 레고 블록. 바퀴든 문이든 밑면 돌기가 같으니, 무엇이든 끼우면 붙는다.

한 줄 예 —

# 아래 예제는 핵심 흐름만 짧게 보여 줍니다.
# 프롬프트도 모델도 파서도 다 "러너블" → 같은 invoke로 부른다
prompt.invoke({"topic": "더블딥"})  # 프롬프트 한 칸 실행
model.invoke("안녕")               # 모델 한 칸 실행

출력 파서(output parser)

한 문장 뜻 — 모델이 뱉은 줄글을 프로그램이 쓰기 좋은 정리된 모양(JSON·표 등)으로 바꿔 주는 부품.

일상비유 — 손님이 말로 늘어놓은 주문을 주방용 주문표 칸에 또박또박 옮겨 적는 점원. 말은 자유롭지만, 표는 칸이 정해져 있다.

한 줄 예 —

# 모델의 줄글 답을 그냥 문자열로 깔끔히 받는다
# 모델 응답을 다음 단계에서 쓰기 쉬운 형태로 정리합니다.
parser = StrOutputParser()

메모리(memory, 대화 기록)

한 문장 뜻 — 챗봇이 방금 전 대화를 기억하게 해, 앞 내용을 이어 말하게 하는 장치.

일상비유 — 통화 중 메모지. "아까 뭐라고 했죠?" 물으면 메모지를 보고 답한다. 메모지가 없으면 매번 처음 본 사람처럼 군다.

한 줄 예 —

# 앞 대화를 적어 두고 다음 질문에 같이 넘긴다
# 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
history.add_user_message("저축 어떻게 늘려요?")

(귀납 도입) OpenAI만 쓰다가 모델을 바꾸려니 코드 절반이 터졌다

처음엔 OpenAI 한 줄로 충분했다.

# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
import openai
# 답변을 생성할 LLM 클라이언트나 모델 설정을 준비합니다.
client = openai.OpenAI()
# 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
client.chat.completions.create(
    # 사용할 모델 이름을 지정해 어떤 LLM으로 답할지 정합니다.
    model="gpt-4o-mini",
    # 모델에게 보낼 대화 메시지 목록을 지정합니다.
    messages=[{"role": "user", "content": "안녕하세요!"}]
)

잘 돌아간다. 그래서 회사 곳곳에 이 호출을 흩뿌렸다.

그런데 어느 날 "Gemini가 더 싸다, 갈아타자" 하는 말이 나왔다.

문제가 터진다. OpenAI 호출법과 Gemini 호출법은 생김새가 다르다. 흩뿌려 둔 그 많은 호출을 하나하나 뜯어고쳐야 한다.

모델 하나 바꾸려다 코드 절반을 다시 짠다.

이 고생을 줄이려고 나온 게 랭체인이다.

랭체인은 모델을 표준 규격으로 한 번 감싼다. 그러면 모델 교체가 블록 하나 갈아 끼우는 일로 줄어든다.

# 랭체인 — 모델은 클래스 한 줄. 교체도 한 줄.
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_openai import ChatOpenAI
# 사용할 모델 이름을 지정해 어떤 LLM으로 답할지 정합니다.
model = ChatOpenAI(model="gpt-4o")
# 다른 모델로? 윗줄만 ChatMistralAI(...) 로 바꾸면 끝.

이 장은 그 "감싸고 끼우는" 방식을 처음부터 본다.


이 장에서 딱 6가지만

  1. 랭체인은 한 덩어리가 아니라 부품 상자(생태계)다. core·langchain·community·파트너·langgraph 등 역할별 블록이 들어 있다.
  2. 랭체인은 모델을 직접 만들지 않는다. OpenAI·Gemini 같은 남의 모델을 표준 규격으로 감싸, 같은 방식으로 부르게 해 준다.
  3. LCEL은 블록을 |(파이프)로 잇는 방식이다. 프롬프트 → 모델 → 파서를 한 줄로 줄 세운다.
  4. 프롬프트 템플릿은 빈칸 양식이다. 빈칸에 값만 갈아 끼워 모델에게 지시를 만든다. 예시를 몇 개 보여 주는 게 퓨샷.
  5. 출력 파서는 모델의 줄글을 정리된 모양으로 바꾼다. JSON·검증된 데이터 등.
  6. 메모리는 챗봇이 앞 대화를 기억하게 한다. 그냥 넘기기 → 차곡차곡 저장 → 자동 관리 → 길면 줄이거나 요약.

여섯 블록을 하나씩 본다. 막히면 0장 용어집으로 돌아가면 된다.


개념 1 — 랭체인은 부품 상자(생태계)다

망가지는 장면

"랭체인 깔았으니 다 되겠지" 하고 한 패키지만 믿었다.

그런데 어떤 기능은 다른 패키지에 있고, 어떤 건 선택 설치다.

뭐가 어디 있는지 모르면, 필요한 블록을 못 찾아 헤맨다.

일상비유

랭체인은 부품이 칸칸이 나뉜 공구 상자다.

드라이버 칸, 나사 칸, 특수 공구 칸이 따로 있다. 칸 이름만 알면 필요한 걸 바로 꺼낸다.

비유 코드 위험
바닥에 깔리는 기본 받침 import langchain_core 없으면 위 블록들이 못 선다
그 위에 얹는 본체 import langchain 깔면 core가 자동으로 같이 깔림
선택 추가 통합 모음 import langchain_community 안 깔면 일부 외부 연결 불가

한 문장 정의 — 랭체인은 하나의 패키지가 아니라, 역할별로 나뉜 여러 패키지를 조합해 쓰는 생태계다.

주요 블록(패키지)을 한눈에

블록 이름 무슨 칸인가
langchain-core 상자 바닥. 모델·검색기·파서의 기본 규격과 LCEL을 정한다. 가볍다.
langchain 본체. 체인·에이전트 같은 앱 뼈대를 만든다. 깔면 core가 함께 깔린다.
langchain-community 커뮤니티가 챙기는 외부 연결 모음. 필요할 때만 선택 설치.
파트너 패키지 자주 쓰는 연결만 따로 뺀 것. langchain-openai·langchain-anthropic 등.
langgraph 갈림길·되돌이가 있는 복잡한 흐름을 그래프로 그릴 때.
langserve 만든 체인을 웹 API로 내보낼 때.
langsmith 잘 도는지 들여다보고 점검할 때(디버깅·모니터링).

예시 폭격 — 의존성 화살표 읽기

(worked) 화살표 "A → B"는 "A가 B에 기댄다"는 뜻이다.

langchain → langchain-core 이므로, langchain만 깔아도 core가 딸려 온다.

# langchain만 깔아도 core가 함께 깔린다
# (직접 의존성: langchain이 core에 직접 기댐)
# 프롬프트·모델·출력 처리를 하나의 실행 흐름으로 이어 붙입니다.
!pip install langchain
import langchain_core  # 따로 안 깔았는데도 쓸 수 있음

(부분 완성) langgraph는 core를 피어 의존성으로 쓴다. 즉 "있으면 골라 쓰는" 사이다.

빈칸을 채워 보자. langgraph는 core를 ____(필수로 / 골라서) 쓴다. → 답: 골라서.

(독립 적용) 친구가 "langchain-anthropic은 뭐예요?" 물으면?

→ "자주 쓰는 외부 연결(앤트로픽 Claude)만 따로 뺀 파트너 패키지예요. 필요할 때만 깔면 됩니다."

버전 이야기 (1.2)

랭체인은 버전이 자주 오른다. 큰 줄기만 잡으면 된다.

  • 0.1 — 패키지를 잘게 나눔(분리 시작).
  • 0.2 — community 의존성을 떼어 더 가벼워짐.
  • 0.3(2024년 9월) — 내부를 Pydantic 2로 바꾸고, Python 3.8 지원을 끝냄.

버전은 자주 바뀌어도 전체 구조는 거의 그대로다. 새 버전이 나오면 바뀐 점만 빠르게 보면 된다.

왜 굳이 랭체인인가 (1.3)

OpenAI만으로도 앱은 만든다. 그래도 랭체인을 쓰는 이유 4가지.

장점 한 줄 뜻
모듈성 기능을 블록으로 끼웠다 뺐다 한다
통합 용이성 OpenAI → Gemini 교체를 설정만 바꿔 한다
확장 기능 LCEL·러너블로 복잡한 흐름을 짧게 쓴다
커뮤니티 예제·문서가 많고 자주 업데이트된다

무엇에 쓰나 (1.4) — RAG와 질의응답, 구조화 출력 뽑기, 챗봇, 도구 사용·에이전트. 이 책의 각 장이 이 쓰임을 하나씩 다룬다.

한 걸음 더 ▸ (지금 몰라도 됨) — 패키지가 많아 보여도, 처음엔 langchain·langchain-core·langchain-openai 세 개면 충분하다. 나머지는 필요할 때 하나씩 꺼내 쓰면 된다.


개념 2 — 모델은 직접 안 만들고, 감싸서 갈아 끼운다

망가지는 장면

OpenAI 호출법으로 코드를 가득 채웠다.

모델을 바꾸려니 호출법이 달라, 곳곳을 다 뜯어고친다.

도입부의 바로 그 고생이다.

일상비유

여러 나라 플러그를 한 모양으로 맞춰 주는 멀티 어댑터.

벽 콘센트가 무엇이든, 어댑터 너머는 늘 같은 모양이라 기기를 그대로 꽂는다.

비유 코드 위험
어댑터 없이 직접 꽂기 client.chat.completions.create(model=...) 모델 바꾸면 곳곳 수정
어댑터로 감싸 꽂기 ChatOpenAI(model="gpt-4o") 교체는 클래스 한 줄만

한 문장 정의 — 랭체인은 LLM을 직접 만들지 않고, OpenAI·Cohere·Hugging Face 같은 여러 제공자를 표준 인터페이스로 감싸 똑같이 다루게 해 준다.

예시 폭격 — 직접 호출 vs 랭체인

(worked) 잘못된 결(특정 모델에 딱 붙음):

# OpenAI 직접 — 모델 교체 시 호출부를 다 고쳐야
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
import openai
# 답변을 생성할 LLM 클라이언트나 모델 설정을 준비합니다.
client = openai.OpenAI()
# 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
client.chat.completions.create(
    # 사용할 모델 이름을 지정해 어떤 LLM으로 답할지 정합니다.
    model="gpt-4o-mini",
    # 모델에게 보낼 대화 메시지 목록을 지정합니다.
    messages=[{"role": "user", "content": "안녕하세요!"}]
)

(worked) 올바른 결(블록 하나만 갈면 됨):

# 랭체인 — 모델은 클래스 한 줄. 교체도 한 줄.
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_openai import ChatOpenAI
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.prompts import ChatPromptTemplate
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.output_parsers import StrOutputParser

# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
prompt = ChatPromptTemplate.from_template("주제 {topic}에 대해 짧은 설명을 해주세요.")
# 사용할 모델 이름을 지정해 어떤 LLM으로 답할지 정합니다.
model = ChatOpenAI(model="gpt-4o")
# 미스트랄로? 윗줄만 ChatMistralAI(...) 로.
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
chain = prompt | model | StrOutputParser()
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
chain.invoke({"topic": "더블딥"})

(부분 완성) 두 방식을 표로 비교해 빈칸을 채워 보자.

항목 OpenAI 직접 랭체인
구조 단순 호출 모듈화된 체인
모델 전환 코드 수정 필요 ____
적합 간단한 작업 복잡·확장 앱

→ 빈칸: 클래스만 교체.

(독립 적용) "간단한 스크립트 하나, 모델 바꿀 일 없음" — 이럴 땐? → 굳이 랭체인 없이 OpenAI 직접 호출도 충분하다. 도구는 일에 맞춰 고른다.

모델의 손잡이들 (파라미터 2.2)

같은 모델도 손잡이를 돌려 답의 결을 바꾼다.

손잡이 무엇을 조절
temperature(0~1) 낮으면 또박또박 일관, 높으면 톡톡 튀게
max_tokens 답 길이의 상한
top_p 상위 확률 단어만 고르기
frequency / presence penalty 반복 줄이기 / 새 단어 장려
stop sequences 특정 글자 만나면 생성 멈추기
# 온도 낮춤 = 매번 비슷한 답, 길이는 100조각까지
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_openai import OpenAI
# 답변을 생성할 LLM 클라이언트나 모델 설정을 준비합니다.
llm = OpenAI(temperature=0.2, max_tokens=100)

어떤 모델을 고르나 (2.3) — 제공자마다 문맥 크기(한 번에 읽는 글 양)·백만 토큰당 입출력 비용·한국어 성능이 다르다.

한국어 성능은 LogicKor 같은 대시보드로 비교한다.

비용과 모델은 수시로 바뀌므로, 쓸 때 공식 가격표를 꼭 확인한다.

한 걸음 더 ▸ — 책의 gpt-4o·Claude 3.5·Gemini는 2025년 기준 예시다. 실제로 쓸 땐 공식 모델 목록을 본다: OpenAI·Anthropic·Google. (검증 2026-05-21)


개념 3 — LCEL: 블록을 |로 한 줄에 줄 세운다

망가지는 장면

프롬프트 만들고, 모델 부르고, 답을 정리하고… 단계가 많다.

이걸 하나하나 손으로 이으면, 중간에 데이터 모양이 안 맞아 자꾸 어긋난다.

일상비유

컨베이어 벨트. 재료를 올리면 자르고 → 굽고 → 포장이 차례로 흐른다.

각 칸은 앞 칸이 넘긴 걸 받아 처리하고 다음 칸에 넘긴다. 사람이 일일이 들고 옮기지 않는다.

비유 코드 위험
벨트로 칸 잇기 chain = prompt \| model \| parser 앞 칸 출력 모양이 다음 칸 입력과 안 맞으면 막힘
벨트에 재료 올리기 chain.invoke({"topic": "더블딥"}) 입력 키 이름 틀리면 빈칸 안 채워짐

한 문장 정의 — LCEL(랭체인 표현 언어)은 러너블 블록을 파이프(|)로 이어, "무엇을 할지"를 선언적으로 한 줄에 쓰는 방식이다.

선언적이란? — "어떻게 하나하나 하라"가 아니라 "무엇을 할지"만 적는 것이다.

벨트 설계도만 그려 두면, 재료가 알아서 흐르는 식이다.

모든 블록이 공유하는 공통 버튼 (러너블 표준 메서드 3.1)

프롬프트·모델·파서가 다 러너블이라, 같은 버튼으로 부른다.

동기 비동기 무슨 일
invoke() ainvoke() 입력 하나 처리
batch() abatch() 여러 입력 한꺼번에
stream() astream() 답을 토막토막 실시간으로
# 같은 체인, 부르는 버튼만 다름
chain.invoke({"topic": "더블딥"})                       # 하나
chain.batch([{"topic": "더블딥"}, {"topic": "인플레이션"}])  # 여럿
for token in chain.stream({"topic": "더블딥"}):          # 토막 실시간
    # 중간 결과를 눈으로 확인해 흐름이 맞는지 봅니다.
    print(token, end="", flush=True)

참고stream()은 답이 다 만들어지기 전에 토막을 흘려보낸다. flush=True는 "화면에 바로바로 보여라"라는 뜻이다. 채팅창에 글자가 또르륵 찍히는 그 느낌이다.

예시 폭격 — 블록 잇는 여러 방법 (3.2)

(worked) 파이프 연산자 — 가장 흔한 방법:

# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_openai import ChatOpenAI
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.prompts import ChatPromptTemplate
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.output_parsers import StrOutputParser

# 사용할 모델 이름을 지정해 어떤 LLM으로 답할지 정합니다.
model = ChatOpenAI(model="gpt-4o-mini")
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
prompt = ChatPromptTemplate.from_template("주제 {topic}에 대해 짧은 설명을 해주세요.")
# 모델 응답을 다음 단계에서 쓰기 쉬운 형태로 정리합니다.
parser = StrOutputParser()
chain = prompt | model | parser   # 이 | 가 "이어 붙여라"
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
chain.invoke({"topic": "더블딥"})

참고 — |는 어떻게 작동하나 — 파이썬은 원래 |로 이런 연결을 안 해 준다. 랭체인이 __or__라는 특별 약속을 덧씌워서(오버로딩) "|를 만나면 블록을 이어라"로 바꿔 둔 것이다. 지금은 "랭체인이 |에 새 뜻을 줬다"만 알면 된다.

(worked) .pipe() 메서드 — 같은 일을 메서드로:

# prompt | model | parser 와 같은 결과
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
chain = prompt.pipe(model).pipe(parser)

(worked) 체인 뒤에 체인 잇기 — 설명을 만들고 영어로 번역까지:

# {"answer": chain} 으로 앞 결과를 받아 → 번역 프롬프트로 넘김
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
analysis_prompt = ChatPromptTemplate.from_template("이 대답을 영어로 번역해 주세요: {answer}")
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
composed = {"answer": chain} | analysis_prompt | model | StrOutputParser()
composed.invoke({"topic": "더블딥"})  # 더블딥 설명을 영어로 받음

(부분 완성) 중간에 손수 만든 함수도 블록으로 끼울 수 있다. 함수는 러너블로 자동 변환된다.

# chain 결과를 {"answer": ...} 모양으로 바꿔 다음에 넘기는 람다
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
composed = chain | (lambda x: {"answer": x}) | analysis_prompt | model | StrOutputParser()

빈칸: 단, 이렇게 람다를 끼우면 ____ 작업과 호환이 안 될 수 있어 주의한다. → 답: 스트리밍.

(독립 적용) "같은 주제를 한국어·영어로 동시에 받고 싶다." → RunnableParallel로 두 체인을 나란히 돌린다.

# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.runnables import RunnableParallel

# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
kor = ChatPromptTemplate.from_template("{topic}에 대해 짧은 설명을 해주세요.") | model | parser
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
eng = ChatPromptTemplate.from_template("{topic}에 대해 짧게 영어로 설명을 해주세요.") | model | parser
# `parallel`에 중간 결과를 담아 다음 줄에서 재사용합니다.
parallel = RunnableParallel(kor=kor, eng=eng)
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
result = parallel.invoke({"topic": "더블딥"})
# result["kor"], result["eng"] 로 둘 다 받음

미니 시나리오 — 사용자가 "응답이 너무 느려요. 다 나올 때까지 빈 화면이에요" 한다.

invoke 대신 stream으로 바꿔, 만들어지는 토막을 바로바로 보여 준다. 체감 속도가 확 산다.


개념 4 — 프롬프트 템플릿: 빈칸 양식

망가지는 장면

주제마다 지시문을 통째로 새로 쓴다. "더블딥 설명해 줘", "인플레이션 설명해 줘"…

같은 문장을 매번 복사·수정한다. 실수도 늘고 지저분하다.

일상비유

빈칸 있는 양식 편지. "주제 ____에 대해 설명해 줘"를 만들어 두고, 빈칸에 단어만 갈아 끼운다.

비유 코드 위험
빈칸 양식 만들기 ChatPromptTemplate.from_template("주제 {topic}…") 빈칸 이름과 입력 키가 다르면 안 채워짐
빈칸에 값 끼우기 prompt.invoke({"topic": "주식"}) 딕셔너리로 줘야 함

한 문장 정의 — 프롬프트 템플릿은 사용자 입력을 모델용 지시문으로 바꾸는 빈칸 양식이며, {변수} 자리에 값을 채워 완성한다. 입력은 딕셔너리로 받는다.

세 가지 양식

양식 언제
PromptTemplate 단순한 한 줄 문자열 지시
ChatPromptTemplate system/user/ai 역할이 있는 대화형
MessagesPlaceholder 대화 이력처럼 목록을 끼울 빈칸

예시 폭격 — 양식 만들기

(worked) 문자열 양식:

# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.prompts import PromptTemplate
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
prompt = PromptTemplate.from_template("주제 {topic}에 대해 짧은 조언을 해주세요.")
prompt.invoke({"topic": "투자"})  # → "주제 투자에 대해 …" 완성

(worked) 챗 양식 — 역할을 나눠 줌:

# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.prompts import ChatPromptTemplate
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 유능한 금융 조언가입니다."),  # AI 역할 고정
    ("user", "주제 {topic}에 대해 조언을 해주세요"),  # 사용자 요청
])
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
prompt.invoke({"topic": "주식"})

(부분 완성) 대화 목록을 통째로 끼울 빈칸:

# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.messages import HumanMessage

# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
prompt = ChatPromptTemplate.from_messages([
    # 이 줄은 예제 흐름을 이루는 실제 처리 단계입니다.
    ("system", "당신은 유능한 금융 조언가입니다."),
    MessagesPlaceholder("msgs"),   # 또는 ("placeholder", "{msgs}")
])
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
prompt.invoke({"msgs": [HumanMessage(content="안녕하세요!")]})

빈칸: MessagesPlaceholder("msgs") 대신 더 짧게 쓰면 ("placeholder", "____") 이다. → 답: {msgs}.

(독립 적용) "예시를 몇 개 보여 주면 모델이 더 잘한다던데?" → 그게 퓨샷이다(아래 4.1).

퓨샷 프롬프트 (4.1)

예시를 보여 주는 개수로 이름이 갈린다.

  • 0개 = 제로샷
  • 1개 = 원샷
  • n개 = 퓨샷(n샷)

예시를 한꺼번에 넣어 주는 게 FewShotPromptTemplate이다.

(worked) 잘못된 결 — 예시를 전부 욱여넣음(토큰 낭비):

# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.prompts import FewShotPromptTemplate
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
prompt = FewShotPromptTemplate(
    examples=examples,          # 예시 전부
    # 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
    example_prompt=example_prompt,
    # 여러 값을 이름표가 붙은 구조로 묶어 전달합니다.
    suffix="질문: {input}",
    # 프롬프트가 바깥에서 받을 입력 이름을 적습니다.
    input_variables=["input"],
)

(worked) 올바른 결 — 예제 선택기로 비슷한 예시만 골라 넣음(토큰 절약):

# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_chroma import Chroma
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.example_selectors import SemanticSimilarityExampleSelector
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_openai import OpenAIEmbeddings

# `selector`에 중간 결과를 담아 다음 줄에서 재사용합니다.
selector = SemanticSimilarityExampleSelector.from_examples(
    # 이 줄은 예제 흐름을 이루는 실제 처리 단계입니다.
    examples,
    OpenAIEmbeddings(api_key=api_key),  # 예시를 숫자로 바꿔
    Chroma,                              # 창고에 넣고 비교
    k=1,                                 # 가장 비슷한 1개만 고름
)

미니 시나리오 — 예시가 50개라 매번 다 넣으니 비용이 폭발한다.

→ 예제 선택기에 k=1을 줘, 질문과 가장 가까운 한 개만 골라 넣는다. 비용이 뚝 떨어진다.

프롬프트 허브 (4.2)

남이 잘 만든 프롬프트를 가져다 쓰는 중앙 창고다.

# 허브 기능은 랭체인 1.0부터 별도 패키지로 분리됨: pip install langchain-classic
from langchain_classic import hub
prompt = hub.pull("hardkothari/prompt-maker")        # 최신 버전 가져오기
prompt = hub.pull("hardkothari/prompt-maker:c5db8eee")  # 특정 버전 고정

:c5db8eee 처럼 버전을 콕 집으면, 나중에 원본이 바뀌어도 내가 쓰던 그 버전을 그대로 쓴다.


개념 5 — 출력 파서: 줄글을 정리된 모양으로

망가지는 장면

모델이 "부동산 투자는 장기적으로 안정적이며…" 하고 줄글로 답했다.

이걸 프로그램에서 "질문 부분"과 "답변 부분"으로 나눠 쓰려는데, 줄글이라 칼같이 안 잘린다.

일상비유

손님의 말 주문을 주방 주문표 칸에 옮겨 적는 점원.

말은 자유롭지만, 표는 '메뉴' 칸 '수량' 칸이 정해져 있어 주방이 헷갈리지 않는다.

비유 코드 위험
줄글을 깔끔한 문자열로 StrOutputParser() 구조가 필요하면 부족함
줄글을 칸 있는 표(JSON)로 JsonOutputParser() 모델이 형식 안 지키면 오류

한 문장 정의 — 출력 파서는 모델의 자유 텍스트를 JSON·CSV·검증된 데이터 같은 구조화된 모양으로 바꿔, 프로그램이 곧장 쓰게 해 준다.

참고 — 요즘 모델은 함수·도구 호출을 직접 지원한다. 그쪽이 가능하면 출력 파서 대신 그걸 권한다. 출력 파서는 그게 안 되거나 단순한 경우에 쓴다.

출력 파서가 가진 세 버튼 (5.1)

버튼 무슨 일
get_format_instructions() "이런 형식으로 답해" 지침을 모델에 줄 문구 만들기
parse() 모델 답(줄글·JSON 문자열) → 파이썬 데이터로 변환
parse_with_prompt() 질문까지 함께 받아, 형식 틀렸을 때 고쳐 다시 시도
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.output_parsers import JsonOutputParser
# 모델 응답을 다음 단계에서 쓰기 쉬운 형태로 정리합니다.
parser = JsonOutputParser()
print(parser.get_format_instructions())  # 모델에 줄 형식 지침
# 줄글 JSON을 파이썬 딕셔너리로
parser.parse('{"이름": "김철수", "나이": 30}')  # → {"이름": "김철수", "나이": 30}

예시 폭격 — 파서 종류별로

(worked) PydanticOutputParser — 칸을 정해 두고 검증까지:

# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.output_parsers import PydanticOutputParser
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from pydantic import BaseModel, Field, model_validator

# 관련 데이터와 동작을 하나의 이름 아래 묶는 클래스입니다.
class FinancialAdvice(BaseModel):
    # `setup: str`에 중간 결과를 담아 다음 줄에서 재사용합니다.
    setup: str = Field(description="금융 조언 상황 질문")
    # `advice: str`에 중간 결과를 담아 다음 줄에서 재사용합니다.
    advice: str = Field(description="질문에 대한 금융 답변")

    # 아래 함수나 클래스를 프레임워크가 특별한 용도로 인식하게 표시합니다.
    @model_validator(mode="before")
    # 아래 함수나 클래스를 프레임워크가 특별한 용도로 인식하게 표시합니다.
    @classmethod
    # 반복해서 쓸 처리 흐름에 이름을 붙인 함수입니다.
    def must_end_with_question(cls, values):
        # 조건을 확인해서 상황에 맞는 처리 경로를 고릅니다.
        if not values.get("setup", "").endswith("?"):
            raise ValueError("질문은 '?'로 끝나야 합니다.")  # 형식 어기면 막음
        # 계산하거나 처리한 결과를 이 함수를 부른 쪽으로 돌려줍니다.
        return values

# 모델 응답을 다음 단계에서 쓰기 쉬운 형태로 정리합니다.
parser = PydanticOutputParser(pydantic_object=FinancialAdvice)

(worked) SimpleJsonOutputParser — 가벼운 JSON, 스트리밍 지원:

# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.output_parsers import SimpleJsonOutputParser
# 모델 응답을 다음 단계에서 쓰기 쉬운 형태로 정리합니다.
json_parser = SimpleJsonOutputParser()
# 값이 한 글자씩 차오르는 걸 실시간으로 볼 수 있음
# {} → {"answer": "비"} → {"answer": "비트"} → …

(부분 완성) JsonOutputParser — JSON 특화. Pydantic 스키마도 결합:

# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.output_parsers import JsonOutputParser
# 모델 응답을 다음 단계에서 쓰기 쉬운 형태로 정리합니다.
parser = JsonOutputParser(pydantic_object=FinancialAdvice)
# 결과는 ____ 모양으로 나온다: {"setup": "...", "advice": "..."}

빈칸: 딕셔너리.

(독립 적용) 파서가 너무 많다. 표로 골라 보자.

파서 스트리밍 검증 언제
StrOutputParser 없음 그냥 문자열
SimpleJsonOutputParser 없음 가벼운 JSON
JsonOutputParser 선택 JSON + 스키마
PydanticOutputParser 제한적 있음 형식 검증이 꼭 필요할 때

미니 시나리오 — 모델이 가끔 ? 없이 질문을 만든다.

PydanticOutputParser에 검증 규칙(?로 끝나야 함)을 넣어, 형식 어긴 답을 걸러 낸다.


개념 6 — 메모리: 챗봇이 앞 대화를 기억한다

망가지는 장면

챗봇에게 "저축 어떻게 늘려요?" 묻고 답을 받았다.

이어 "방금 뭐라고 했죠?" 하니, "무슨 말씀이신지 모르겠어요" 한다.

방금 한 말을 전혀 기억 못 한다. 매 질문이 첫 만남이다.

일상비유

통화 중 메모지. 앞에 한 말을 적어 두고, "아까 뭐랬죠?" 하면 메모지를 보고 답한다.

메모지가 없으면 매번 처음 통화하는 사람처럼 군다.

비유 코드 위험
메모지를 같이 건넴 ("placeholder", "{messages}") 안 건네면 앞 대화 까먹음
메모지가 너무 길어짐 트리밍·요약으로 줄임 안 줄이면 느려지고 비싸짐

한 문장 정의 — 메모리는 챗봇이 이전 대화를 기억해 맥락을 이어 가게 하는 장치이며, 그냥 넘기기 → 저장 → 자동 관리 → 줄이기·요약, 점점 더 손이 덜 가게 발전한다.

예시 폭격 — 점점 자동으로 (네 단계)

(worked) 1단계 — 그냥 넘기기(6.1): 앞 대화를 손수 프롬프트 빈칸에 넣는다.

# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
prompt = ChatPromptTemplate.from_messages([
    # 이 줄은 예제 흐름을 이루는 실제 처리 단계입니다.
    ("system", "당신은 금융 상담사입니다."),
    ("placeholder", "{messages}"),   # 여기에 대화 이력을 통째로
])
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
chain = prompt | chat
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
chain.invoke({"messages": [
    # 이 줄은 예제 흐름을 이루는 실제 처리 단계입니다.
    ("human", "저축을 늘리려면?"),
    # 이 줄은 예제 흐름을 이루는 실제 처리 단계입니다.
    ("ai", "매달 자동이체로 저축하세요."),
    ("human", "방금 뭐라고 했죠?"),   # 앞 두 줄을 같이 줘서 기억함
]})

(worked) 2단계 — ChatMessageHistory(6.2): 대화를 차곡차곡 저장한다.

# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_community.chat_message_histories import ChatMessageHistory
# `history`에 중간 결과를 담아 다음 줄에서 재사용합니다.
history = ChatMessageHistory()
# 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
history.add_user_message("저축을 늘리려면?")
# 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
history.add_ai_message("매달 자동이체로 저축하세요.")
# 다음 질문 때 history.messages 를 넘기면 됨

(부분 완성) 3단계 — RunnableWithMessageHistory(6.3): 체인을 감싸 자동 저장·불러오기. 아직 동작하지만 공식적으로 은퇴 예고된 부품이다(2.0에서 제거 예정) — 요즘 공식 권장은 6장에서 배울 LangGraph의 체크포인터(대화 저장소)다. 여기서는 개념 이해용으로만 보자.

# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.runnables.history import RunnableWithMessageHistory
# 프롬프트·모델·출력 처리를 하나의 실행 흐름으로 이어 붙입니다.
chain_auto = RunnableWithMessageHistory(
    # 프롬프트·모델·출력 처리를 하나의 실행 흐름으로 이어 붙입니다.
    chain,
    # 이 줄은 예제 흐름을 이루는 실제 처리 단계입니다.
    lambda session_id: history,
    # 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
    input_messages_key="input",
    # 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
    history_messages_key="chat_history",
)
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
chain_auto.invoke(
    # 여러 값을 이름표가 붙은 구조로 묶어 전달합니다.
    {"input": "저축을 늘리려면?"},
    {"configurable": {"session_id": "____"}},  # 대화방을 구분하는 표
)

빈칸: session_id에는 대화방마다 다른 이름을 준다(예: "session_1"). 같은 이름이면 같은 메모지를 공유한다.

(독립 적용) 4단계 — 대화가 100턴 넘게 길어지면? 두 가지로 줄인다.

# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.messages import trim_messages
# 트리밍 — 최근 2개만 남기고 잘라 냄
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
trimmer = trim_messages(strategy="last", max_tokens=2, token_counter=len)
# 요약 — 옛 대화를 압축해 핵심만 남김 (요약 프롬프트로 모델이 줄임)

미니 시나리오 — 상담 챗봇이 너무 느리고 토큰비가 많이 나온다.

트리밍은 빠르지만 오래된 맥락을 잃는다. 요약은 토큰을 크게 아끼지만 중요 정보가 빠질 위험이 있다.

초반 맥락이 중요하면 요약을, 속도가 중요하면 트리밍을 고른다. 둘은 트레이드오프다.

한 걸음 더 ▸ (지금 몰라도 됨) — 요즘 권장은 대화 상태를 langgraph로 관리하는 쪽으로 옮겨 가고 있다. 개념은 이 절로 충분하다. 신규 코드를 짤 땐 공식 문서의 최신 권장을 확인하면 된다.


단순 규칙 (헷갈리면 이대로)

  • 모델은 직접 호출하지 말고 체인으로: prompt | model | parser. 나중에 교체가 쉽다.
  • 일관된 답이 필요하면 temperature=0, 창의적이면 높인다.
  • 구조화된 답이 필요하면 출력 파서(또는 가능하면 함수·도구 호출).
  • 챗봇의 대화 기억은 LangGraph 체크포인터(6장)가 현행 공식 권장. (RunnableWithMessageHistory는 은퇴 예고 상태 — 기존 코드 읽을 때만 알아두자.)
  • 대화가 길어지면 트리밍이나 요약으로 줄인다.
  • 모델·가격은 공식 페이지에서 확인 — 책 표기는 2025년 예시다.

정리 (핵심 3줄)

랭체인은 모델·프롬프트·파서·메모리를 같은 규격의 블록(러너블)으로 감싸, |(LCEL)로 한 줄에 잇게 해 주는 부품 상자다.

덕분에 모델 교체는 블록 하나 갈아 끼우기로, 복잡한 흐름은 짧은 한 줄로 줄어든다.

이 여섯 블록(생태계·모델·LCEL·프롬프트·파서·메모리)이 다음에 만들 RAG의 바탕이 된다.


더 해보기

다음 2장에서는 이 블록들을 진짜로 끼워, 문서를 읽어 답하는 첫 RAG를 직접 만든다. (지금 6블록만 손에 있으면 충분합니다.)


연습문제

  1. 설명. 랭체인이라는 레고 상자의 핵심을 처음 듣는 사람에게 한 문장으로 설명하라.
  2. 구분. 두 개념(LCEL 체인, 직접 호출)을 실제 예시 하나로 구분하라.
  3. 적용. 내 프로젝트나 학습 노트에서 이 장의 개념을 적용해 작게 개선할 지점을 하나 고르라.

부록 A. 쉬운 용어 사전

용어 아주 쉬운 뜻 이 장에서 나온 위치
LCEL 체인 랭체인 부품을 |로 이어 하나의 처리 흐름처럼 만든 것. 부록 B와 본문 예시
직접 호출 체인이나 공통 규격 없이 모델 API나 함수를 바로 부르는 방식. 부록 B와 본문 예시
프롬프트 템플릿 매번 달라지는 입력을 빈칸에 넣어 같은 질문 양식을 만드는 틀. 부록 B와 본문 예시
출력 파서 모델 답을 표, JSON, 목록처럼 쓰기 쉬운 모양으로 정리하는 부품. 부록 B와 본문 예시

부록 B. 헷갈리는 개념 비교표

A B 구분 포인트
LCEL 체인 직접 호출 체인은 부품을 바꾸기 쉽고, 직접 호출은 빠르지만 흐름이 흩어진다.
프롬프트 템플릿 출력 파서 앞은 질문 양식, 뒤는 답변 모양 정리 담당이다.

부록 C. 더 읽을 자료

  • 이 장의 더 해보기 섹션 — 이미 모아 둔 공식 문서나 실습 링크가 있으면 여기서 먼저 확인한다.
  • 같은 책의 0장 한눈에 보기 — 용어가 막히면 0장의 용어집과 개념 척추로 돌아간다.
  • 원본 딥다이브판 같은 장 — 입문판을 읽고 큰 흐름이 잡힌 뒤 세부 논리를 더 깊게 확인한다.
  • 이 장의 flashcards.json — 읽은 직후 질문만 보고 답을 떠올리는 회상 연습에 쓴다.

부록 D. 연습문제 풀이

  1. 설명 예시. 랭체인이라는 레고 상자는 RAG에서 자료를 더 잘 찾고, 근거를 더 안전하게 붙이고, 답변 흐름을 더 다루기 쉽게 만드는 방법을 보는 장이다. 중요한 것은 용어를 외우는 것이 아니라, 이 개념이 어떤 입력·부품·결정에 영향을 주는지 말로 풀어 보는 것이다.
  2. 구분 예시. 두 개념(LCEL 체인, 직접 호출)의 차이는 이렇게 잡으면 된다. 체인은 부품을 바꾸기 쉽고, 직접 호출은 빠르지만 흐름이 흩어진다. 실제 사례를 볼 때는 목적, 입력, 실패했을 때의 증상을 따로 적어 보면 헷갈리지 않는다.
  3. 적용 예시. 가장 작은 개선부터 고른다. 예를 들어 이름을 더 분명히 하거나, 평가 기준을 한 줄 추가하거나, 직접 알 필요 없는 내부 정보를 감추는 식으로 시작한다. 한 번에 크게 갈아엎는 것보다 작은 변경 하나를 확인하며 진행하는 쪽이 입문 단계에 맞다.
난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플